第一個要下載的資料,就是台灣所有上市公司的代號 vs. 名稱,因為在後續的資料中,可能有些資料只會有代號,也可能有些資料只會有名稱,所以這個鐵人賽第一步,決定先下載股票代號對照表。而下面所提到的資料表,連公司基本資訊都有。
這些資料放在政府的資料開放平台上,不過我們只需要下載位置就可以了。
上市公司資料
https://mopsfin.twse.com.tw/opendata/t187ap03_L.csv
上櫃公司資料
https://mopsfin.twse.com.tw/opendata/t187ap03_O.csv
興櫃公司資料
https://mopsfin.twse.com.tw/opendata/t187ap03_R.csv
決定好目標之後,先分析需要哪些 column。
下載完 csv 檔之後,我會需要[公司代號]、[公司名稱]、[公司簡稱]、[資本額],這些欄位。
那裝載資料的 Data Model 就會是這樣
import Foundation
struct StockBasicInfo {
let stockCode: String
let stockName: String
let companyName: String
let capital: String
}
這個 csv 檔在 iOS 這邊也有套件可以處理,我選擇用 SwiftCSV
安裝方法說明文件在下方 repo 的 readme
https://github.com/swiftcsv/SwiftCSV
和 Alamofire 一樣,我們要有個 adapter 來包第三方套件。
import Foundation
import SwiftCSV
struct CSVAdapter {
var header = [String]()
var namedRows = [[String: String]]()
var namedColumns = [String: [String]]()
init?(rawString: String) {
if let csv = try? CSV(string: rawString) {
self.header = csv.header
self.namedRows = csv.namedRows
self.namedColumns = csv.namedColumns
}
}
}
接下來就是寫一個專門處理股票資訊的類別,在基本資料上,因為上市、上櫃、興櫃的資料被放在三個不同的地方,所以在 request function 上,會先分成三個 func,後續可以看情況再決定是否要再開一個 func 對這三個 func 進行連續呼叫。
import Foundation
/// 這一個類別主要處理上市上櫃興興公司相關資料
class StockInfoManager {
private lazy var alamofireAdapter: AlamofireAdapter = {
return AlamofireAdapter()
}()
/// 取得上市公司基本資料
func requestTwStockCodeAndName(completion: @escaping (([StockBasicInfo], Error?) -> Void)) {
let urlString = "https://mopsfin.twse.com.tw/opendata/t187ap03_L.csv"
requestStockInfoBasic(urlString) { list, error in
completion(list, error)
}
}
/// 取得上櫃公司基本資料
func requestOTCCodeAndName(completion: @escaping (([StockBasicInfo], Error?) -> Void)) {
let urlString = "https://mopsfin.twse.com.tw/opendata/t187ap03_O.csv"
requestStockInfoBasic(urlString) { list, error in
completion(list, error)
}
}
/// 取得興櫃公司基本資料
func requestEmerginCodeAndName(completion: @escaping (([StockBasicInfo], Error?) -> Void)) {
let urlString = "https://mopsfin.twse.com.tw/opendata/t187ap03_R.csv"
requestStockInfoBasic(urlString) { list, error in
completion(list, error)
}
}
private func requestStockInfoBasic(_ urlString: String, completion: @escaping ([StockBasicInfo], Error?) -> Void) {
var companyList = [StockBasicInfo]()
alamofireAdapter.request(urlString, method: .get) { data, response, error in
if let error = error {
print("tw stock fetch error: \(error.localizedDescription)")
completion(companyList, error)
return
}
if let data = data,
let string = String(data: data, encoding: .utf8),
let csv = CSVAdapter(rawString: string) {
for company in csv.namedRows {
let stockCode = company["公司代號"] ?? ""
let stockName = company["公司簡稱"] ?? ""
let companyName = company["公司名稱"] ?? ""
let capital = company["實收資本額"] ?? ""
let info = StockBasicInfo(stockCode: stockCode, stockName: stockName, companyName: companyName, capital: capital)
companyList.append(info)
}
}
completion(companyList, nil)
}
}
}
當完成後,可以在 VC 裡面試著打 request,看有沒有回應。可以試著印出 csv.headers,如果有印出下列文字,就表示有拿到資料
["出表日期", "公司代號", "公司名稱", "公司簡稱", "外國企業註冊地國", "產業別", "住址", "營利事業統一編號", "董事長", "總經理", "發言人", "發言人職稱", "代理發言人", "總機電話", "成立日期", "上市日期", "普通股每股面額", "實收資本額", "私募股數", "特別股", "編制財務報表類型", "股票過戶機構", "過戶電話", "過戶地址", "簽證會計師事務所", "簽證會計師1", "簽證會計師2", "英文簡稱", "英文通訊地址", "傳真機號碼", "電子郵件信箱", "網址"]
而使用 csv.namedRows 就可以操作 csv 檔裡面的 row。如果要操作 column,就用 csv.namedColumns。
試著印出第 0 個 namedRows,資訊應該如下。
["過戶電話": "66365566", "簽證會計師事務所": "勤業眾信聯合會計師事務所", "發言人": "黃健強", "過戶地址": "台北市重慶南路一段83號5樓", "住址": "台北市中山北路2段113號", "外國企業註冊地國": "- ", "總機電話": "(02)2531-7099", "實收資本額": "61574403270", "成立日期": "19501229", "簽證會計師1": "邵志明", "公司代號": "1101", "編制財務報表類型": "1", "營利事業統一編號": "11913502", "傳真機號碼": "(02)2531-6529", "特別股": "200000000", "電子郵件信箱": "finance@taiwancement.com", "股票過戶機構": "中國信託商業銀行代理部", "總經理": "李鐘培", "上市日期": "19620209", "產業別": "01", "公司名稱": "台灣水泥股份有限公司", "網址": "http://www.taiwancement.com", "出表日期": "1100903", "英文簡稱": "TCC", "公司簡稱": "台泥", "普通股每股面額": "新台幣 10.0000元", "簽證會計師2": "黃惠敏", "發言人職稱": "資深副總經理", "代理發言人": "賴家柔", "董事長": "張安平", "英文通訊地址": "No.113, Sec.2, Zhongshan N. Rd.,Taipei City 104,Taiwan (R.O.C.)", "私募股數": "0"]